/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package controller;

import digitrencer.CompositeElement;
import digitrencer.Model;
import digitrencer.Wire;
import java.io.File;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.ArrayList;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

/**
 * A hálózat beolvasását végzi az XML formátumú fájlból.
 * @author Balázs
 */
public class XMLParser {

    private DocumentBuilderFactory dbFactory;
    private DocumentBuilder parser;
    private PrintWriter logfile;
    private ArrayList<Integer> inputs, outputs;     // referenciák a Controller listáira

    /**
     * Konstruktor, inicializálja az XMLParser objektumot.
     * @param inputs A Controller inputs listájának referenciája.
     * @param outputs A Controller outputs listájának referenciája.
     * @param logfile A kimeneti fájl.
     * @throws ParserConfigurationException
     */
    public XMLParser(ArrayList<Integer> inputs, ArrayList<Integer> outputs, PrintWriter logfile) throws ParserConfigurationException {
        this.inputs = inputs;
        this.outputs = outputs;
        this.logfile = logfile;
        dbFactory = DocumentBuilderFactory.newInstance();
        dbFactory.setIgnoringElementContentWhitespace(true);
        parser = dbFactory.newDocumentBuilder();
    }

    /**
     * Az XML fájl beolvasását végző függvény. Lényegében ez építi fel a teljes hálózatot.
     * @param file A beolvasni kívánt XML fájl.
     * @return A felépített Model.
     * @throws SAXException Amennyiben az XML feldolgozása során hiba történt, pl hibás szintaxis esetén.
     * @throws IOException Amennyiben nem sikerült a fájl olvasása.
     * @throws NoSuchFieldException Hiányzó mező, attribútum vagy kompozit elem definíció esetén.
     * @throws IllegalArgumentException Nem megfelelő jelgenerátor sorozat vagy pin esetén.
     */
//Beletúram egy kicsit, hogy az új grafikus felületről és a régi konzolos parancsok hatására is működjön. - Kéki
    public Model LoadXML(/*String*/Object file) throws SAXException, IOException,
            NoSuchFieldException, IllegalArgumentException {
        //Buherált, átmeneti kódrészlet
        Document doc;
        if (file instanceof String) {        //Tudom, hogy tilos instanceof-ot használni, de ez úgy is csak átmeneti kódrész így egyszerű.
            doc = parser.parse((String) file);
        } else {
            doc = parser.parse((File) file);
        }
        //Hibakezelés nincs. Úgy is csak átmeneti kód.
        //Buharált, átmeneti kódrészlet vége
        //Eredeti kód: Document doc = parser.parse(file);
        org.w3c.dom.Element root = doc.getDocumentElement(); // A gyökér elem lekérése
        Model model = new Model(new Wire[0], new Wire[0]);   // A model létrehozása
        buildCompositeElement(model, root);
        return model;
    }

    /**
     * Egy kompozit elem (vagy akár maga a Model) felépítését végzi.
     * A kompozit elem konstruktorát a függvényen kívül kell meghívni, ez a függvény csak
     * hozzáadja a megadott kompozit elemhez az építőelemeket.
     * @param ce A felépítendő kompozit elem.
     * @param node Az XML fájl gyökér eleme, vagy a kompozit elem definíciója.
     * @throws NoSuchFieldException Hiányzó mező, attribútum vagy kompozit elem definíció esetén.
     * @throws NumberFormatException Nem megfelelő számformátum esetén.
     * @throws IllegalArgumentException Nem megfelelő jelgenerátor sorozat vagy pin esetén.
     */
    private void buildCompositeElement(CompositeElement ce, org.w3c.dom.Element node)
            throws NoSuchFieldException, NumberFormatException, IllegalArgumentException {
        // A wires mező lekérdezése, a vezetékek hozzáadása
        NodeList nl = getChildrenByTagName(node, "wires");
        if (nl == null || nl.getLength() == 0) {
            // Amennyiben nincs wires mező
            String errorString;
            if (node.getLocalName().equals("root")) {
                errorString = "The root element is missing a \"wires\" field";
            } else {
                errorString = "Composite element \"" + node.getAttribute("type") + "\"is missing a \"wires\" field";
            }
            throw new NoSuchFieldException(errorString);
        }
        org.w3c.dom.Element field_wires = (org.w3c.dom.Element) nl.item(0);
        try {
            int num_wires = Integer.parseInt(field_wires.getFirstChild().getNodeValue());
            for (int i = 0; i < num_wires; i++) {
                ce.addWire();
            }
        } catch (NullPointerException e) {
            throw new NoSuchFieldException("\"wires\" field is empty.");
        }

        // Az element-ek beolvasása
        nl = getChildrenByTagName(node, "element");
        if (nl != null) {
            for (int i = 0; i < nl.getLength(); i++) {
                org.w3c.dom.Element field_element = (org.w3c.dom.Element) nl.item(i);
                parseElement(ce, field_element);
            }
        }

        // A composite_element-ek beolvasása
        nl = getChildrenByTagName(node, "composite_element");
        if (nl != null) {
            for (int i = 0; i < nl.getLength(); i++) {
                org.w3c.dom.Element field_composite = (org.w3c.dom.Element) nl.item(i);
                parseCompositeElement(ce, field_composite);
            }
        }
    }

    /**
     * Egy (nem kompozit) elem beolvasását végzi, majd beilleszti az őt tartalmazó kompozit elembe.
     * @param parent A kompozit elem, amelybe beillesztjük az új alkatrészt.
     * @param node Az új alkatrész leírása az XML fájlban.
     * @throws NumberFormatException Nem megfelelő számformátum esetén.
     * @throws NoSuchFieldException Üres mező esetén.
     * @throws IllegalArgumentException Nem megfelelő jelgenerátor sorozat vagy pin esetén.
     */
    private void parseElement(CompositeElement parent, org.w3c.dom.Element node)
            throws NumberFormatException, NoSuchFieldException, IllegalArgumentException {
        String type = node.getAttribute("type");

        // A kapcsolódó vezetékek beolvasása
        int[] elementInputs, elementOutputs;
        elementInputs = createArrayFromChildNodes(node, "in");
        elementOutputs = createArrayFromChildNodes(node, "out");
        int ID = parent.addElement(type, elementInputs, elementOutputs);

        // Inputok és outputok kiíratása, regisztrálása
        if (type.equals("switch") || type.equals("generator")) {
            logMessage("I/O\t:\tInput#" + ID + "(" + type + ")");
            inputs.add(ID);
        }
        if (type.equals("led") || type.equals("oscilloscope")) {
            logMessage("I/O\t:\tOutput#" + ID + "(" + type + ")");
            outputs.add(ID);
        }

        // Szekvencia beállítása jelgenerátor esetén
        // Ellenőrizni: még véletlenül se forduljon elő, hogy egy CompositeElement-ben (helytelenül)
        // deklarált Input vagy Output a listába kerüljön
        String seq = getTextValue(node, "sequence");
        if (seq != null) {
            if (type.equals("generator")) {
                Model model = (Model) parent;
                model.setSequence(ID, seq);     // ismét egy veszélyes rész, ellenőrizni kell
            }
        }

        // Pin beállítása Tag esetén
        String str_pin = getTextValue(node, "pin");
        if (str_pin != null) {
            int pin = Integer.parseInt(str_pin);
            if (type.equals("tag")) {
                parent.setPin(ID, pin);
            }
        }
    }

    /**
     * Egy kompozit elem beolvasását végzi a fájlból. Megkeresi a kompozit elem definícióját,
     * felépíti azt, majd behelyezi az őt tartalmaző kompozit elembe.
     * @param parent Az új kompozit elemet tartalmazó kompozit elem.
     * @param node Az új kompozit elem leírása az XML fájlban.
     * @throws NoSuchFieldException Hiányzó mező, attribútum vagy kompozit elem definíció esetén.
     * @throws NumberFormatException Nem megfelelő számformátum esetén.
     * @throws IllegalArgumentException Nem megfelelő jelgenerátor sorozat vagy pin esetén.
     */
    private void parseCompositeElement(CompositeElement parent, org.w3c.dom.Element node)
            throws NoSuchFieldException, NumberFormatException, IllegalArgumentException {
        String type = node.getAttribute("type");
        if (type == null || type.equals("")) {  // Ha nincs definiálva a típusa
            throw new NoSuchFieldException("Composite element is missing a \"type\" attribute");
        }

        // Definíció megkeresése
        org.w3c.dom.Element definition = findDefinition(node, type);
        if (definition == null) {
            throw new NoSuchFieldException("Composite element \"" + type + "\" has no definiton");
        }
        CompositeElement child = new CompositeElement(new Wire[0], new Wire[0]);
        buildCompositeElement(child, definition);

        // A kapcsolódó vezetékek beolvasása
        int[][] elementInputs, elementOutputs;
        elementInputs = createDoubleArrayFromChildNodes(node, "in", "pin");
        elementOutputs = createDoubleArrayFromChildNodes(node, "out", "pin");
        parent.addComopsiteElement(child, elementInputs, elementOutputs);
    }

    /**
     * Megkeresi egy kompozit elem definícióját.
     * @param composite_node A kompozit elem példányosítását tartalmazó XML elem.
     * @param type A kompozit elem típusa.
     * @return A kompozit elem definícióját tartalmazó XML elem. Ha nincs ilyen, akkor null.
     */
    private org.w3c.dom.Element findDefinition(org.w3c.dom.Element composite_node, String type) {
        org.w3c.dom.Element parent = (org.w3c.dom.Element) composite_node.getParentNode();
        if (parent == null) {
            return null;
        }
        NodeList nl = getChildrenByTagName(parent, "define");
        if (nl != null) {
            for (int i = 0; i < nl.getLength(); i++) {
                org.w3c.dom.Element node = (org.w3c.dom.Element) nl.item(i);
                String define_type = node.getAttribute("type");
                if (type.equals(define_type)) {
                    return node;
                }
            }
        }
        return null;
    }

    /**
     * Egy egész számokat tartalmazó tömböt készít egy XML elem adott típusú mezőiből.
     * Ez az alkatrészek be- és kimeneteinek feldolgozását könnyíti meg.
     * @param node Az XML elem, amelyet feldolgozunk.
     * @param child_type Megadja, hogy az XML elem milyen típusú mezőiből építsük fel a listát.
     *      Ez jellemzően "in" vagy "out".
     * @return A felépített tömb.
     * @throws NumberFormatException Nem megfelelő számformátum esetén.
     * @throws NoSuchFieldException Üres mező esetén.
     */
    private int[] createArrayFromChildNodes(org.w3c.dom.Element node, String child_type)
            throws NumberFormatException, NoSuchFieldException {
        int[] array;
        NodeList nl = getChildrenByTagName(node, child_type);
        if (nl != null) {
            array = new int[nl.getLength()];
            for (int i = 0; i < nl.getLength(); i++) {
                org.w3c.dom.Element field = (org.w3c.dom.Element) nl.item(i);
                try {
                    int ID = Integer.parseInt(field.getFirstChild().getNodeValue());
                    array[i] = ID;
                } catch (NullPointerException e) {
                    throw new NoSuchFieldException("\"" + child_type + "\" is empty.");
                }
            }
        } else {
            array = new int[0];  // A biztonság kedvéért
        }
        return array;
    }

    /**
     * Egy egész számokat tartalmazó kétdimenziós tömböt készit egy XML elem adott típusú mezőiből..
     * A kompozit elem be- és kimeneteinek feldolgozásánál használjuk, ahol a vezetékek mellett attribútumban
     * meg van adva, hogy mely vezeték meylik lábra csatlakozik. Az elkészült kétdimenziós tömb
     * érték-attribútum párokat tartalmaz.
     * @param node Az XML elem, amelyet feldolgozunk.
     * @param child_type Megadja, hogy az XML elem milyen típusú mezőiből építsük fel a listát.
     *      Ez jellemzően "in" vagy "out".
     * @param attribute Megadja a feldolgozni kívánt attribútum nevét.
     *      Ez jellemzően "pin".
     * @return A felépített kétdimenziós tömb.
     * @throws NoSuchFieldException Hiányzó attribútum esetén.
     * @throws NumberFormatException Nem megfelelő számformátum esetén.
     */
    private int[][] createDoubleArrayFromChildNodes(org.w3c.dom.Element node, String child_type, String attribute)
            throws NoSuchFieldException, NumberFormatException {
        int[][] array;
        NodeList nl = getChildrenByTagName(node, child_type);
        if (nl != null) {
            array = new int[nl.getLength()][2];
            for (int i = 0; i < nl.getLength(); i++) {
                org.w3c.dom.Element field = (org.w3c.dom.Element) nl.item(i);
                try {
                    int ID = Integer.parseInt(field.getFirstChild().getNodeValue());
                    array[i][1] = ID;
                } catch (NullPointerException e) {
                    throw new NoSuchFieldException("\"" + child_type + "\" is empty.");
                }
                String str_att = field.getAttribute(attribute);
                if (str_att.equals("")) // Ha nincs megadva pin attribútum
                {
                    throw new NoSuchFieldException("Pin undefined for composite element of type \"" + node.getAttribute("type") + "\"");
                }
                int att = Integer.parseInt(str_att);
                array[i][0] = att;
            }
        } else {
            array = new int[0][2];  // A biztonság kedvéért
        }
        return array;
    }

    /**
     * Egy XML elem adott típusú mezőjének értékét adja vissza.
     * @param node Az XML elem.
     * @param tagName Az XML elem mezőjének neve.
     * @return A kiolvasott érték. Amennyiben a megadott mező nem létezik, a visszatérési érték null.
     * @throws NoSuchFieldException Üres mező esetén.
     */
    private String getTextValue(org.w3c.dom.Element node, String tagName)
            throws NoSuchFieldException {
        String textVal = null;
        NodeList nl = getChildrenByTagName(node, tagName);
        if (nl != null && nl.getLength() > 0) {
            org.w3c.dom.Element el = (org.w3c.dom.Element) nl.item(0);
            try {
                textVal = el.getFirstChild().getNodeValue();
            } catch (NullPointerException e) {
                throw new NoSuchFieldException("\"" + tagName + "\" is empty.");
            }
        }
        return textVal;
    }

    /**
     * Egy XML elem adott típusú gyerekeit adja vissza. Ez helyettesíti a getElementsByTagName
     * függvényt, mivel az nem csak a közvetlen gyerekeit adja vissza az XML elemnek.
     * @param node Az XML elem, amelynek a gyerekeit vizgáljuk.
     * @param tagName A gyerekek típusa.
     * @return A megfelelő típusú gyerekeket tartalmazó NodeList.
     */
    private NodeList getChildrenByTagName(org.w3c.dom.Element node, String tagName) {
        NodeList nl = node.getElementsByTagName(tagName);   // Az összes adott típusú elem lekérdezése
        if (nl == null) {
            return nl;
        }
        NodeArrayList children = new NodeArrayList();
        for (int i = 0; i < nl.getLength(); i++) {
            org.w3c.dom.Element child = (org.w3c.dom.Element) nl.item(i);
            if (child.getParentNode().equals(node)) {   // Szűrés, akkor marad a listában, ha a szülője node
                children.add(child);
            }
        }
        return children;

    }

    /**
     * Kiírja a megadott üzenetet a standard kimenetre és a logfile-ba.
     * @param msg A kiírandó üzenet.
     */
    private void logMessage(String msg) {
        System.out.println(msg);
        if (logfile != null) {
            logfile.println(msg);
        }
    }

    /**
     * Egy NodeList interfészt implementáló segédosztály.
     */
    private class NodeArrayList extends ArrayList<Node> implements NodeList {

        @Override
        public Node item(int index) {
            return get(index);
        }

        @Override
        public int getLength() {
            return size();
        }
    }
}
